﻿namespace ManagedChangeJournal
{
    using System;
    using System.IO;
    using System.ComponentModel;    
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using ManagedChangeJournal.Native.Win32;
    using ManagedChangeJournal.Native.Journal;

    /// <summary>
    /// 
    /// </summary>
    public partial class ChangeJournal : IDisposable
    {
        #region Variables

        /// <summary>
        /// The size, in bytes, to use for buffering 
        /// entries when reading from the opened journal.
        /// </summary>
        private int propBufferLength = 8192;
        /// <summary>
        /// The handle to the currently opened journal volume.
        /// </summary>
        private IntPtr propJournalHandle = new IntPtr(-1);

        #endregion Variables      
        
        #region Constructor / Desctructor

        /// <summary>
        /// Constructs a new instance of this class.
        /// </summary>
        public ChangeJournal()
        {
            Data = new JournalData();
            Entries = new List<JournalEntry>();
            IsOpen = false;
            IsDisposed = false;
        }
        /// <summary>
        /// Destructs the instance of this class.
        /// </summary>
        ~ChangeJournal()
        {
            if ( propJournalHandle != new IntPtr( -1 ) )
            {
                Win32Methods.CloseHandle( propJournalHandle );
                propJournalHandle = new IntPtr( -1 );
            }
        }

        #endregion
        
        #region Properties

        /// <summary>
        /// Gets the JournalData of the open journal.
        /// </summary>
        public JournalData Data { get; private set; }
        /// <summary>
        /// Gets the last set of entries read from the open journal.
        /// </summary>
        public List<JournalEntry> Entries { get; private set; }
        /// <summary>
        /// Gets a value indicating whether a journal volume is open, or not.
        /// </summary>
        public bool IsOpen { get; private set; }
        /// <summary>
        /// Gets or sets a value indicating whether 
        /// this instance has been disposed of, or not.
        /// </summary>
        private bool IsDisposed { get; set; }

        #endregion Properties

        #region Public Methods
        
        /// <summary>
        /// Opens the specified volume, and it's journal if active.
        /// </summary>
        /// <param name="drive">A properly formatted volume string.</param>
        public void Open(string volume)
        {
            if (IsDisposed) 
                throw new ObjectDisposedException(ChangeJournalString);
            if (volume == null)
                throw new ArgumentNullException(Volume);

            if (!VolumeHelper.VolumePathIsValid(volume))
                throw new ArgumentException(Volume,
                    InvalidVolumeFormat);

            if (IsOpen)
                Close();

            propJournalHandle = Win32Methods.CreateFile(volume,
                    FileAccess.Read, FileShare.Read | FileShare.Write,
                    IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);

            if (propJournalHandle.ToInt32() == -1)
                throw new Win32Exception(Marshal.GetLastWin32Error());

            IsOpen = true;
            Query();
        }
        /// <summary>
        /// Closes the currently open journal and volume.
        /// </summary>
        public void Close()
        {
            if (IsDisposed)
                throw new ObjectDisposedException("ChangeJournal");
            if (!IsOpen)
                throw new InvalidOperationException(
                    CanCloseOnlyWhenOpen);

            if (!Win32Methods.CloseHandle(propJournalHandle))
                throw new Win32Exception(Marshal.GetLastWin32Error());

            Entries.Clear();
            Data = new JournalData();
            IsOpen = false;
            propJournalHandle = new IntPtr(-1);           
        }
        /// <summary>
        /// Creates a journal for the open volume - if a journal
        /// already exists and the specified MaximumSize is larger 
        /// the journal will be expanded.
        /// </summary>
        /// <param name="MaximumSize">The maximum size,
        /// in bytes, the created journal can reach before
        /// reallocation takes place.</param>
        /// <param name="AllocationDelta">The size,
        /// in bytes, of the allocation delta to be used 
        /// when reallocation takes place.</param>
        public void Create(long MaximumSize, long AllocationDelta)
        {
            if (IsDisposed)
                throw new ObjectDisposedException(ChangeJournalString);
            if (!IsOpen)
                throw new InvalidOperationException(
                    CanCreateOnlyWhenOpen);            
            if (AllocationDelta < MaximumSize && AllocationDelta != 0)
                throw new Exception(GreaterAllocationDeltaRequired);            
            if (MaximumSize < 4096 && MaximumSize != 0)
                throw new Exception(DecentMaximumSizeOrZeroRequired);
            if (AllocationDelta < 4096 && AllocationDelta != 0)
                throw new Exception(DecentAllocationDeltaOrZeroRequired); 

            var creationData = new CreationData();
            creationData.AllocationDelta = AllocationDelta;
            creationData.MaximumSize = MaximumSize;

            Win32DeviceControl.ControlWithInput<CreationData>(
                propJournalHandle, Win32ControlCode.CreateUsnJournal, 
                ref creationData, 0);

            Query();
        }
        /// <summary>
        /// Deletes the currently open journal, leaving the volume open.
        /// </summary>
        public void Delete()
        {
            if (IsDisposed) 
                throw new ObjectDisposedException(
                    ChangeJournalString);
            if (!IsOpen)
                throw new InvalidOperationException(
                    CanDeleteOnlyWhenOpen);
            if (!Data.IsActive)
                throw new InvalidOperationException(
                    CanDeleteOnlyWhenActive);

            var deletionData = new DeletionData();
            deletionData.UsnJournalID = (ulong)Data.ID;
            deletionData.DeleteFlags = 
                (int)DeletionFlag.WaitUntilDeleteCompletes;

            Win32DeviceControl.ControlWithInput<DeletionData>(
                propJournalHandle, Win32ControlCode.CreateUsnJournal,
                ref deletionData, 0);
            
            Query();
        }
        /// <summary>
        /// Queries the currently open journal, populating the Data property.
        /// </summary>
        public void Query()
        {
            if (IsDisposed) 
                throw new ObjectDisposedException(ChangeJournalString);
            if (!IsOpen)
                throw new InvalidOperationException(CanQueryOnlyWhenOpen);

            try
            {
                var journalData = new Native.Journal.JournalData();
                Win32DeviceControl.ControlWithOutput<Native.Journal.JournalData>(
                    propJournalHandle, Win32ControlCode.QueryUsnJournal, ref journalData);

                Data = new JournalData(journalData);
                Data.CurrentUSN = Data.FirstUSN;
                Data.IsActive = true;
                Data.AtEndOfJournal = false;
            }
            catch (Win32Exception win32ex)
            {
                switch (win32ex.NativeErrorCode)
                {
                    case (int)Win32ErrorCode.ERROR_JOURNAL_NOT_ACTIVE:
                        Data = new JournalData();
                        break;
                    default: throw;
                }
            }
        }
        /// <summary>
        /// Reads the next set of records from the currently open journal,
        /// if the end of the journal was reached with the last read then 
        /// the journal is re-queried and reading starts from the first USN.
        /// </summary>
        /// <param name="reasonFilter"></param>
        /// <param name="returnOnlyOnClose"></param>
        /// <param name="timeout"></param>
        public void Read(ChangeReason reasonFilter,
            bool returnOnlyOnClose, long timeout)
        {
            if (IsDisposed) 
                throw new ObjectDisposedException(
                    ChangeJournalString);
            if (!IsOpen)
                throw new InvalidOperationException(
                    CanReadOnlyWhenOpen);
            if (!Data.IsActive)
                throw new InvalidOperationException(
                    CanReadOnlyWhenActive);

            Entries.Clear();

            var readData = new ReadData();
            if (Data.AtEndOfJournal)
            {
                Query();
            }
            readData.StartUsn = Data.CurrentUSN;
            readData.ReasonMask = reasonFilter;
            readData.UsnJournalID = (ulong)Data.ID;
            readData.ReturnOnlyOnClose = returnOnlyOnClose ? (uint)1 : 0;
            readData.Timeout = (ulong)timeout;

            byte[] entryData = Win32DeviceControl.ControlWithInput<ReadData>(
                propJournalHandle, Win32ControlCode.ReadUsnJournal,
                ref readData, propBufferLength);

            if (entryData.Length > sizeof(long))
            {
                var bufferHandle = GCHandle.Alloc(entryData, GCHandleType.Pinned);
                var bufferPointer = bufferHandle.AddrOfPinnedObject();
                //set StartUsn for next read as the next usn
                Data.CurrentUSN = Marshal.ReadInt64(entryData, 0);
                //enumerate collected entries
                long offset = sizeof(long);               
                while (offset < entryData.Length)
                {
                    var entry = GetBufferedEntry(bufferPointer, offset);
                    offset += entry.Length;
                    Entries.Add(entry);
                }
                bufferHandle.Free();
            }
            else
            {
                Data.AtEndOfJournal = true;
            }
        }

        #endregion Public Methods

        #region Private Methods
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="offset"></param>
        /// <returns></returns>
        private JournalEntry GetBufferedEntry(IntPtr bufferPointer, long offset)
        {
            var entryPointer = new IntPtr(bufferPointer.ToInt32() + offset);
            var nativeEntry = (Native.Journal.JournalEntry)
                Marshal.PtrToStructure(entryPointer,
                typeof(Native.Journal.JournalEntry));
            var filenamePointer = new IntPtr(bufferPointer.ToInt32() +
                offset + nativeEntry.FileNameOffset);
            return new JournalEntry(nativeEntry, 
                Marshal.PtrToStringAuto(filenamePointer));
        }

        #endregion Private Methods

        #region IDisposable Members

        /// <summary>
        /// Disposes of this instance, if nto already disposed.
        /// </summary>
        public void Dispose()
        {
            if (!IsDisposed)
            {
                Close();
                IsDisposed = true;
            }
            else
            {
                throw new ObjectDisposedException("ChangeJournal");
            }
        }

        #endregion

        #region Strings

        string Volume = "volume";
        string ChangeJournalString = "ChangeJournal";
        string InvalidVolumeFormat = "The specified value " +
            "must be a properly formatted volume string, for example: \\\\.\\X.";
        string CanCreateOnlyWhenOpen = "A volume must be open in order to create a journal.";
        string CanCloseOnlyWhenOpen = "A volume must be open in order to close it.";
        string DecentMaximumSizeOrZeroRequired = "MaximumSize must be a reasonable value (4096 or above), or 0 for default.";
        string DecentAllocationDeltaOrZeroRequired = "AllocationDelta must be a reasonable value (4096 or above), or 0 for default.";
        string GreaterAllocationDeltaRequired = "AllocationDelta must be greater than MaximumSize, or zero";
        string CanDeleteOnlyWhenOpen = "A volume must be open in order to delete it's journal.";
        string CanDeleteOnlyWhenActive = "A volume's journal can only be deleted when it is active.";
        string CanQueryOnlyWhenOpen = "A volume must be open in order to query it's journal.";
        string CanReadOnlyWhenOpen = "A volume must be open in order to read it's journal.";
        string CanReadOnlyWhenActive = "A volume's journal must be active in order to read it.";

        #endregion
    }
}
